package net;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

import core.Main;

import messages.Notification;

import util.Debug;

/**
 * Manages single connections between nodes.
 * TODO: Wait for connection, handle idleness, receive and send messages
 * 
 * @author Emil Wall
 */
public class Connection extends Thread {
	
	private final int SLEEP_MS = 500;
	private ServerSocket serverSocket;
	private Socket socket = null;
	private ObjectInputStream ois = null;
	private ObjectOutputStream oos = null;
	private String hostName = null;
	
	private String ip = "localhost";
	private int initPort;
	private String hostAddress;

	private Main master;
	private String name;
	private Boolean shouldRun = true;
	
	/**
	 * Constructor for incoming connection
	 */
	public Connection(ServerSocket serverSocket, Main master) {
		this.serverSocket = serverSocket;
		this.name = ""+serverSocket.getLocalPort();
		shouldRun = true;
		this.master = master;
	}
	
	/**
	 * Constructor for outgoing connection
	 */
	public Connection(String ip, int init_port, String name, Main master) {
		if (ip != null) {
			this.ip = ip;
		}
		initPort = init_port;
		this.master = master;
		setHostAddress(this.ip);
		this.serverSocket = null;
		shouldRun = true;
	}
	
	public void run() {
		if(serverSocket != null) {
			master.writeToLog(name, "Waiting for connection");
			try {
				socket = serverSocket.accept();
				String address = socket.getInetAddress().getHostAddress();
				int port = socket.getPort();
				hostName = address+":"+port;
				master.writeToLog(name, "Connected to "+hostName);
			} catch (IOException e) {
				if(Debug.on) {
					master.writeToLog(name, "failed to connect.");
				}
				if(Debug.verbose) {
					e.printStackTrace();
				}
			}
			bindIOStreams();
			
			while(true) {
				try {
					Thread.sleep(SLEEP_MS);
				} catch (InterruptedException e) {
					if(Debug.on && Debug.verbose) {
						e.printStackTrace();
					}
				}
			}
		} else {
			while (true) {
				if (!shouldRun) {
					disconnect();
					master.writeToLog(name, "Disconnecting...");
					break;
				}
				if (connectionIsAlive()) {
					// Connected, so we try send/receive data
					// TODO: read from input stream
				} else {
					// Disconnected, so we try to connect.
					connect();
					bindIOStreams();
				}

				try {
					if (Debug.on) {
						// master.writeToLog(name"Connector thread sleeping...");
					}
					Thread.sleep(SLEEP_MS);
				} catch (InterruptedException e) {
					if (Debug.on && Debug.verbose) {
						e.printStackTrace();
					}
				}
			}
		}
	}
	

	/**
	 * Checks connection status by trying to write to socket
	 * 
	 * @return true if the connection is alive, else false
	 */
	private boolean connectionIsAlive() {
		if (socket == null || !socket.isConnected()) {
			return false;
		} else if (ois == null || oos == null) {
			return false;
		} else {
			// TODO use streams to check connection status
			return true;
		}

	}
	
	/**
	 * Resolve name and address
	 * 
	 * @param serverIP
	 */
	private void setHostAddress(String serverIP) {
		InetAddress host = null;
		if (serverIP == "localhost") {
			try {
				host = InetAddress.getLocalHost();
			} catch (UnknownHostException e) {
				if (Debug.on) {
					System.err.println("Unable to connect to localhost.");
				}
				if (Debug.verbose) {
					e.printStackTrace();
				}
			}
		} else {
			try {
				host = InetAddress.getByName(serverIP);
			} catch (UnknownHostException e) {
				if (Debug.on) {
					System.err.println("Unable to resolve name or server "
							+ serverIP);
				}
				if (Debug.verbose) {
					e.printStackTrace();
				}
			}
		}
		if (host != null) {
			hostAddress = host.getHostAddress();
		} else if (Debug.on) {
			System.err.println("Host name resolving failed");
		}
	}
	
	/**
	 * Open a connection to the server socket, or close it if it's already open.
	 * 
	 * @return a connected socket, or null if connection failed.
	 */
	public Socket connect() {
		connect(initPort);
		bindIOStreams();
		int port = 0;
		try {
			while(ois.available() == 0) {
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			port = ois.readInt();
			master.writeToLog(name, "Assigned port " + port + " by daemon");
		} catch (IOException e) {
			if (Debug.verbose) {
				master.writeToLog(name, "Failed to read from ois");
				e.printStackTrace();
			}
			return null;
		} catch (NullPointerException e) {
			if (Debug.verbose) {
				master.writeToLog(name, "ois status: " + ois);
				if (ois != null) {
					e.printStackTrace();
				}
			}
			return null;
		}
		socket = connect(port);
		String ip = socket.getInetAddress().getHostAddress();
		this.name = ip+":"+port;
		//		master.writeToLog(name, socket.toString());
		//		master.writeToLog(name, "Socket status: ");
		return socket;
	}
	
	private Socket connect(int port) {
		disconnect(); // Clean up from previous connections
		if (socket == null || !socket.isConnected()) {
			try {
				hostName = hostAddress + ":" + port;
				socket = new Socket(hostAddress, port);
				name = ""+socket.getLocalPort();
				// socket.setKeepAlive(true);
				if (Debug.on) {
					master.writeToLog(name, "Connected to "+hostName);
				}
			} catch (UnknownHostException e) {
				if (Debug.on) {
					master.writeToLog(name, "Connection to " + hostAddress
							+ " failed: " + "Unknown host");
					System.err.println("connect failed: " + e.getMessage());
				}
				if (Debug.verbose) {
					e.printStackTrace();
				}
				return null;
			} catch (IOException e) {
				if (Debug.on) {
					master.writeToLog(name, "Connection to " + hostAddress
							+ " failed");
//					master.writeToLog(name,
//							"connect: Failed.\n" + e.getMessage());
				}
//				if(Debug.verbose) {
//					e.printStackTrace();
//				}
				return null;
			}
		} else if (Debug.on) {
			master.writeToLog(name, "Already connected!");
		}
		return socket;
	}
	
	
	/**
	 * Dispose system resources
	 */
	private void disconnect() {
		try {
			// Close input stream first or deadlock will occur.
			if (ois != null) {
				ois.close();
				ois = null;
			}
			if (oos != null) {
				oos.close();
				oos = null;
			}
			if (socket != null && socket.isConnected()) {
				socket.close();
			}
			socket = null;
		} catch (Exception e) {
			if (Debug.on) {
				master.writeToLog(name, "disconnect: " + e.getMessage());
			}
			return;
		}
	}
	
	/**
	 * Sets a new running status and returns the old one
	 * @param shouldRun new running status
	 * @return old running status
	 */
	public boolean shouldRun(boolean shouldRun) {
		synchronized(this.shouldRun) {
			boolean oldStatus = this.shouldRun;
			this.shouldRun = shouldRun;
			return oldStatus;
		}
	}
	
	/**
	 * Get state
	 * 
	 * @return true if thread should continue running, else false
	 */
	public boolean shouldRun() {
		synchronized(this.shouldRun) {
			return shouldRun;
		}
	}
	
	/**
	 * Assigns class variables oos and ois [ObjectInputStream & 
	 * ObjectInputStream]
	 */
	private void bindIOStreams() {
		if(socket != null && socket.isConnected()) {
			try {
				// Get the output and input streams. 
				// Initialize output stream first or deadlock will occur.
				if(oos == null) {
					oos = new ObjectOutputStream(socket.getOutputStream());
				}
				if(ois == null) {
					ois = new ObjectInputStream(socket.getInputStream());
				}
			} catch (IOException e) {
				if(Debug.on) {
					System.err.println("bindIOStreams:" + e.getMessage());
				}
			}
		}
	}
	
	/**
	 * Returns a string representation of the remote host address
	 * @return address formatted as "ip:port"
	 */
	public String getAddress() {
		for(int i=0; socket == null || !socket.isConnected() && i<10; i++) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		if(socket == null) {
			return "disconnected:0";
		}
		String ip = socket.getInetAddress().getHostAddress();
		if(ip == null) {
			ip = "disconnected";
		}
		String port = ""+socket.getPort();
		return ip+":"+port;
	}
	
	/**
	 * Attempt to send a serializable object over the stream.
	 * @author Erik, Emil
	 */
	public void sendObject(Object obj) throws IOException {
		if(oos != null) {
			oos.writeObject(obj);
		} else {
			throw new IOException("No active object output stream");
		}
	}
	
	public void notify(Notification ad) {
		try {
			sendObject((Object)ad);
		} catch (IOException e) {
			if(Debug.verbose) {
				e.printStackTrace();
			}
		}
	}
	
	public String getHost() {
		if(hostName != null) {
			return hostName;
		}
		for(int i=0; (socket == null || !socket.isConnected()) && i<10; i++) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		if (socket !=null && socket.isConnected()) {
			hostName = ip+":"+socket.getPort();
			return hostName;
		} else {
			return "localhost:0";
		}
	}
	
}
